package ownLinkedlist;

import org.apache.log4j.Logger;

import java.util.Iterator;

public class LinkedList<E> {
    private static final Logger logger = Logger.getLogger(LinkedList.class);

    private int listSize;
    private Node headNode;
    private Node tailNode;

    public Iterator<E> iterator() {
        return new ListIterator();
    }

    private class Node<E> {
        private Node leftLink;
        private Node rightLink;
        private E element;

        private Node(E inputElement) {
            element = inputElement;
        }

        public Node getLeftLink() {
            return leftLink;
        }

        public void setLeftLink(Node leftLink) {
            this.leftLink = leftLink;
        }

        public Node getRightLink() {
            return rightLink;
        }

        public void setRightLink(Node rightLink) {
            this.rightLink = rightLink;
        }

        public E getElement() {
            return element;
        }
    }

    private class ListIterator implements OwnIterator {
        private Node currentIterationNode;
        private boolean isAscending;

        public Node getCurrentIterationNode() {
            return currentIterationNode;
        }

        @Override
        public Object getElement() {
            return currentIterationNode.getElement();
        }

        public ListIterator() {
            currentIterationNode = headNode;
            isAscending = true;
        }

        public ListIterator(Node node) {
            currentIterationNode = node;
            isAscending = true;
        }

        //*****Ascend

        @Override
        public boolean isAscending() {
            return isAscending;
        }

        @Override
        public void switchToAscending() {
            isAscending = true;
        }

        @Override
        public void switchToDescending() {
            isAscending = false;
        }

        //*****Next-Prev
        @Override
        public boolean hasNext() {
            if (isAscending) {
                return currentIterationNode.getRightLink() != null;
            } else //if descending
                return currentIterationNode.getLeftLink() != null;
        }

        @Override
        public boolean hasPrevious() {
            if (isAscending) {
                return currentIterationNode.getLeftLink() != null;
            } else //if descending
                return currentIterationNode.getRightLink() != null;
        }
        //*****Movenents
        @Override
        public Object next() {
            if (isAscending)
                currentIterationNode = currentIterationNode.getRightLink();
            else
                currentIterationNode = currentIterationNode.getLeftLink();
            return currentIterationNode;
        }

        @Override
        public Object previous() {
            if (isAscending) //if descending
                currentIterationNode = currentIterationNode.getLeftLink();
            else
                currentIterationNode = currentIterationNode.getRightLink();
            return currentIterationNode;
        }

        @Override
        public void remove() {
            removeNode(currentIterationNode);
        }

        @Override
        public void moveToHead() {
            currentIterationNode = headNode;
        }

        @Override
        public void moveToTail() {
            currentIterationNode = tailNode;
        }
    }

    public LinkedList(E[] inputArray) {
        if (inputArray != null) {
            if (inputArray.length >= 1) {
                headNode = tailNode = new Node(inputArray[0]);
                listSize++;

                if (inputArray.length > 1) {
                    Node pivotalNode = headNode;
                    for (int i = 1; i < inputArray.length; i++) {
                        pivotalNode = tailNode = addNewNodeAfter(pivotalNode, inputArray[i]);
                    }
                }
            }
        }
    }

    //*****General
    public int getListSize() {
        return listSize;
    }

    public E getHeadNode() {
        return (E) headNode.getElement();
    }

    public E getTailNode() {
        return (E) tailNode.getElement();
    }

    public boolean isEmpty() {
        return listSize == 0;
    }

    public void clear() {
        headNode = null;
        tailNode = null;
        listSize = 0;
        logger.info("List cleared.");
    }

    private int getNodeIndex(Node node) {
        if (node != null) {
            ListIterator li = new ListIterator(node);
            int index = 0;
            while (li.hasPrevious()) {
                index--;
                li.previous();
            }
            return -index;
        }
        throw new NullPointerException();
    }

    /**
     * If index negative it will return head node.
     * If index is more then listSize, it will return tail node.
     * If list is empty, it will return null.
     *
     * @param index node index in current method call list state.
     * @return Link to <Node> object.
     */
    public Node getNodeByIndex(int index) {
        if (listSize == 0)
            return null;

        if (index >= (listSize - 1))
            return tailNode;

        if ((index > 0) && (listSize > 0)) {
            ListIterator li = new ListIterator();
            for (int i = 0; i < index; i++) {
                li.next();
            }
            return li.getCurrentIterationNode();
        }
        return headNode; // negative or 0 index
    }

    //*****Addition
    private Node addNewNodeBefore(Node existNodeToAdd, E element) {
        if ((existNodeToAdd != null) && (element != null)) {
            logger.info("Adding new node to left side of the node with current index " + getNodeIndex(existNodeToAdd) + ".");
            Node newNode = new Node(element);

            if (existNodeToAdd.getLeftLink() != null)
                newNode.setLeftLink(existNodeToAdd.getLeftLink());
            else
                newNode.setLeftLink(null);

            newNode.setRightLink(existNodeToAdd);
            existNodeToAdd.setLeftLink(newNode);
            listSize++;
            logger.info("New node added.");
            return newNode;
        }
        throw new NullPointerException();
    }

    private Node addNewNodeAfter(Node existNodeToAdd, E element) {
        if ((existNodeToAdd != null) && (element != null)) {
            logger.info("Adding new node to right side of the node with current index " + getNodeIndex(existNodeToAdd) + ".");
            Node newNode = new Node(element);
            newNode.setLeftLink(existNodeToAdd);

            if (existNodeToAdd.getRightLink() != null)
                newNode.setRightLink(existNodeToAdd.getRightLink());
            else
                newNode.setRightLink(null);
            existNodeToAdd.setRightLink(newNode);
            listSize++;
            logger.info("New node added.");
            return newNode;
        }
        throw new NullPointerException();
    }

    public void addNewFirst(E newElement) {
        if (newElement != null) {
            headNode = addNewNodeBefore(headNode, newElement);
            logger.info("New head node added with index 0.");
        }
    }

    public void add(E newElement) {
        if (newElement != null) {
            tailNode = addNewNodeAfter(tailNode, newElement);
            logger.info("New tail node added with index " + getNodeIndex(tailNode) + ".");
        }
    }

    //*****Remove
    private boolean removeNodeLinks(Node nodeToRemove) {
        if (nodeToRemove != null) {
            nodeToRemove.setLeftLink(null);
            nodeToRemove.setRightLink(null);
            return true;
        }
        return false;
    }

    private boolean removeNode(Node nodeToRemove) {
        if (nodeToRemove != null) {
            logger.info("Removing node with current index " + getNodeIndex(nodeToRemove) + ".");
            Node left = nodeToRemove.getLeftLink();
            Node right = nodeToRemove.getRightLink();

            if ((left != null) || (right != null)) {
                if (left == null) {
                    right.setLeftLink(null);
                    headNode = right;
                } else if (right == null) {
                    left.setRightLink(null);
                    tailNode = left;
                } else {
                    left.setRightLink(right);
                    right.setLeftLink(left);
                }
            }
            //elseif (left == null) && (right == null):
            removeNodeLinks(nodeToRemove);
            listSize--;
            logger.info("Node removed.");
            return true;
        }
        return false;
    }

    public boolean removeNodeByIndex(int index) {
        return removeNode(getNodeByIndex(index));
    }

    //*****Elements
    public boolean contains(Object obj) {
        if ((obj != null) && listSize != 0) {
            if (obj.getClass() == headNode.getElement().getClass()) {
                ListIterator li = new ListIterator();
                if (li.getCurrentIterationNode().getElement().equals(obj))
                    return true;
                while (li.hasNext()) {
                    li.next();
                    if (li.getCurrentIterationNode().getElement().equals(obj))
                        return true;
                }
            }
        }
        return false;
    }

    public E getElement(int index) {
        return (E) getNodeByIndex(index).getElement();
    }

    @Override
    public String toString() {
        StringBuilder out = new StringBuilder();

        out.append("HEAD > ").append(headNode.getElement());
        ListIterator li = new ListIterator();
        while (li.hasNext()) {
            li.next();
            out.append("||").append(li.getCurrentIterationNode().getElement());
        }
        out.append(" < TAIL");

        return out.toString();
    }
}
